package com.yuxl.common.core.config;

import com.yuxl.common.core.annotation.MySqlRepository;
import com.yuxl.common.core.annotation.PostgreSqlRepository;
import dev.miku.r2dbc.mysql.MySqlConnectionFactoryProvider;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Option;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.*;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.util.StringUtils;

import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * @author yuxl
 */
@Configuration
@EnableConfigurationProperties
public class DataSourceConfig {

    // PostgreSQL数据源配置
    @Configuration
    @EnableR2dbcRepositories(
            basePackages = "com.yuxl",
            entityOperationsRef = "postgreSqlEntityTemplate",
            repositoryImplementationPostfix = "PostgreSqlImpl",
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = PostgreSqlRepository.class)
    )
    public static class PostgreSqlDataSourceConfig extends AbstractR2dbcConfiguration {

        @Primary
        @Bean(name = "postgreSqlProperties")
        @ConfigurationProperties(prefix = "spring.postgresql")
        public R2dbcProperties postgreSqlProperties() {
            return new R2dbcProperties();
        }

        @Primary
        @Override
        @Bean(name = "postgreSqlConnectionFactory")
        public ConnectionFactory connectionFactory() {
            R2dbcProperties properties = postgreSqlProperties();
            ConnectionFactoryOptions connectionFactoryOptions = getConnectionFactoryOptions(properties);
            return new PostgresqlConnectionFactory(PostgresqlConnectionFactoryProvider.builder(connectionFactoryOptions).build());
        }

        @Primary
        @Bean(name = "postgreSqlTransactionManager")
        public ReactiveTransactionManager postgreSqlTransactionManager(
                @Qualifier("postgreSqlConnectionFactory") ConnectionFactory connectionFactory) {
            return new R2dbcTransactionManager(connectionFactory);
        }

        @Primary
        @Bean(name = "postgreSqlDatabaseClient")
        public DatabaseClient postgreSqlDatabaseClient(
                @Qualifier("postgreSqlConnectionFactory") ConnectionFactory connectionFactory) {
            return DatabaseClient.create(connectionFactory);
        }

        @Bean(name = "postgreSqlEntityTemplate")
        public R2dbcEntityTemplate postgreSqlEntityTemplate(
                @Qualifier("postgreSqlConnectionFactory") ConnectionFactory connectionFactory) {
            return new R2dbcEntityTemplate(connectionFactory);
        }
    }

    // MySQL数据源配置
    @Configuration
    @EnableR2dbcRepositories(
            basePackages = "com.yuxl",
            entityOperationsRef = "mySqlEntityTemplate",
            repositoryImplementationPostfix = "MySqlImpl",
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MySqlRepository.class)
    )
    public static class MySqlDataSourceConfig extends AbstractR2dbcConfiguration {

        @Bean(name = "mySqlProperties")
        @ConfigurationProperties(prefix = "spring.mysql")
        public R2dbcProperties mySqlProperties() {
            return new R2dbcProperties();
        }

        @Override
        @Bean(name = "mySqlConnectionFactory")
        public ConnectionFactory connectionFactory() {
            R2dbcProperties properties = mySqlProperties();
            ConnectionFactoryOptions connectionFactoryOptions = getConnectionFactoryOptions(properties);
            return new MySqlConnectionFactoryProvider().create(connectionFactoryOptions);
        }

        @Bean(name = "mySqlTransactionManager")
        public ReactiveTransactionManager mySqlTransactionManager(
                @Qualifier("mySqlConnectionFactory") ConnectionFactory connectionFactory) {
            return new R2dbcTransactionManager(connectionFactory);
        }

        @Bean(name = "mySqlDatabaseClient")
        public DatabaseClient mySqlDatabaseClient(
                @Qualifier("mySqlConnectionFactory") ConnectionFactory connectionFactory) {
            return DatabaseClient.create(connectionFactory);
        }

        @Bean(name = "mySqlEntityTemplate")
        public R2dbcEntityTemplate mySqlEntityTemplate(
                @Qualifier("mySqlConnectionFactory") ConnectionFactory connectionFactory) {
            return new R2dbcEntityTemplate(connectionFactory);
        }
    }

    /**
     * PostgreSQL仓库过滤器，只处理带有PostgreSqlRepository注解的接口
     */
    @Bean
    public PostgreSqlRepositoryFilter postgreSqlRepositoryFilter() {
        return new PostgreSqlRepositoryFilter();
    }

    /**
     * MySQL仓库过滤器，只处理带有MySqlRepository注解的接口
     */
    @Bean
    public MySqlRepositoryFilter mySqlRepositoryFilter() {
        return new MySqlRepositoryFilter();
    }

    public static ConnectionFactoryOptions getConnectionFactoryOptions(R2dbcProperties properties) {
        ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(properties.getUrl());
        ConnectionFactoryOptions.Builder optionsBuilder = urlOptions.mutate();
        configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, properties::getUsername,
                StringUtils::hasText);
        configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, properties::getPassword,
                StringUtils::hasText);
        configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE,
                () -> determineDatabaseName(properties), StringUtils::hasText);
        if (properties.getProperties() != null) {
            properties.getProperties()
                    .forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value));
        }
        return optionsBuilder.build();
    }

    private static <T extends CharSequence> void configureIf(ConnectionFactoryOptions.Builder optionsBuilder,
                                                      ConnectionFactoryOptions originalOptions, Option<T> option, Supplier<T> valueSupplier,
                                                      Predicate<T> setIf) {
        if (originalOptions.hasOption(option)) {
            return;
        }
        T value = valueSupplier.get();
        if (setIf.test(value)) {
            optionsBuilder.option(option, value);
        }
    }

    private static String determineDatabaseName(R2dbcProperties properties) {
        if (properties.isGenerateUniqueName()) {
            return properties.determineUniqueName();
        }
        if (StringUtils.hasLength(properties.getName())) {
            return properties.getName();
        }
        return null;
    }
}